//
// dates.cpp
// libprotea
// Tools Library for the Protea Project / Date-handling functions for Fields
// Copyright (C) 2003, 2004, 2005 eXegeSys, Inc.
// Copyright (C) 2008 Bruce A. James
//
// This program is part of the Protea Project.
//
// The Protea Project is free software; you can redistribute it and/or modify it 
// under the terms of the GNU General Public License as published by the 
// Free Software Foundation, either version 3 of the License, or (at your 
// option) any later version.
//
// The Protea Project is distributed in the hope that it will be useful, but 
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
// for more details.
// 
// You should have received a copy of the GNU General Public License along 
// with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// Please send correspondence to:
// 
// theproteaproject@gmail.com
//

#include "../defines.h"
#include <stdlib.h>
#include <math.h>
#include "fields.h"
#include "dates.h"

const char DATE_SEP = '/';
const char ALT_DATE_SEP = '-';

const long NINETEEN_HUNDRED = 1900;
const long TWO_THOUSAND = 2000;

const long BAD_JD_LO = 0;		//	12/31/1949
const long BAD_JD_HI = 5373180;	//	01/01/9999

const long MIDDLE_2D_YEAR = 50;
const long MIN_2D_YEAR = 0;
const long MAX_2D_YEAR = 99;

const long MIN_4D_YEAR = 1900;
const long MAX_4D_YEAR = 9999;

const long DATE_ERR_10 = 10;
const long DATE_ERR_20 = 20;
const long DATE_ERR_21 = 21;
const long DATE_ERR_22 = 22;
const long DATE_ERR_25 = 25;
const long DATE_ERR_40 = 40;
const long DATE_ERR_200 = 200;
const long DATE_ERR_210 = 210;
const long DATE_ERR_211 = 211;
const long DATE_ERR_212 = 212;
const long DATE_ERR_215 = 215;
const long DATE_ERR_216 = 216;
const long DATE_ERR_220 = 220;
const long DATE_ERR_310	= 310;
const long DATE_ERR_311 = 311;
const long DATE_ERR_312 = 312;
const long DATE_ERR_313 = 313;
const long DATE_ERR_315 = 315;
const long DATE_ERR_316 = 316;
const long DATE_ERR_317 = 317;
const long DATE_ERR_320 = 320;
const long DATE_ERR_321 = 321;
const long DATE_ERR_322 = 322;
const long DATE_ERR_323 = 323;
const long DATE_ERR_325 = 325;
const long DATE_ERR_326 = 326;
const long DATE_ERR_327 = 327;
const long DATE_ERR_328 = 328;
const long DATE_ERR_329 = 329;
const long DATE_ERR_330 = 330;


const char short_month_name[ 12 ][4] =  {
	"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
};

const char full_month_name[ 12 ][12] = {
	"JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER",
	"OCTOBER", "NOVEMBER", "DECEMBER"
};




double round( double value ) {
	double frac, ignore;
	frac = modf( value, &ignore );
	if (frac >= 0) {
		if (frac > 0.5)
			return ceil( value );
		else
			return floor( value );
	} else {
		if (frac > -0.5)
			return ceil( value );
		else
			return floor( value );
	};
};





void do_leading_zero( const date_form_type& date_form, int& length, char*& text_ptr ) {
	if (date_form.lead_zero == 0 &&	length == 1) {
		*text_ptr = *(text_ptr-1);		// Copy digit
		*(text_ptr-1) = '0';			// Leading ascii zero
		text_ptr++;
		length++;
	};
};




bool put_day( int day, char*& text_ptr, const date_form_type& date_form, long index, long& err_num ) {
	int length;

	// CONVERT DAY TO TEXT AND MOVE TO BYTE LOCATION POINTED TO BY TEXT'PTR.
	// UPDATE THE TEXT'PTR. RETURN FALSE IF ANY ERROR IS DETECTED, OTHERWISE
	// RETURN TRUE.
	if (day < 1 || day > 32) {
		err_num = DATE_ERR_210;		// BAD DAY VALUE
		return false;
	};

	length = sprintf( text_ptr, "%d", day );
	text_ptr += length;
	do_leading_zero( date_form, length, text_ptr );

	return true;
};	// Put_Day





bool put_month( int month, char*& text_ptr, const date_form_type& date_form, long index, long& err_num ) {
	static char SHORT_MONTH_NAME[37] = 
		"JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC";
	static char FULL_MONTH_NAME[75] = 
		"JANUARYFEBRUARYMARCHAPRILMAYJUNEJULYAUGUSTSEPTEMBEROCTOBERNOVEMBERDECEMBER";

	// MONTH INDEX INTO FULL'MONTH'NAME
	static int MONTH_INDEX[13] = { 0, 7, 15, 20, 25, 28, 32, 36, 42, 51, 58, 66, 74 };
	int length;

	// CONVERT MONTH TO TEXT AND MOVE TO BYTE LOCATION POINTED TO BY TEXT'PTR,
	// ACCORDING TO MONTH'FORM.  UPDATE THE TEXT'PTR. RETURN FALSE IF ANY
	// ERROR IS DETECTED, OTHERWISE RETURN TRUE.
	if (month < 1 || month > 12) {
		err_num = DATE_ERR_211;	// BAD MONTH VALUE
		return false;
	};

	switch( date_form.date_type[index] ) {
		case 3: // MM Format
			length = sprintf( text_ptr, "%d", month );
			text_ptr += length;
			do_leading_zero( date_form, length, text_ptr );
			break;

		case 4: // MON FORMAT
			memcpy( text_ptr, &SHORT_MONTH_NAME[ 3 * (month-1) ], 3 );
			text_ptr += 3;
			break;

		case 5: // FULLMONTH FORMAT
			length = MONTH_INDEX[ month ] - MONTH_INDEX[ month - 1 ];
			memcpy( text_ptr, &FULL_MONTH_NAME[ MONTH_INDEX[ month - 1 ] ], length );
			text_ptr += length;
		break;
	};

	return true;
};	// Put_Month




bool put_year( int year, char*& text_ptr, const date_form_type& date_form, long index, long& err_num ) {
	// CONVERT YEAR TO TEXT AND MOVE TO BYTE LOCATION POINTED TO BY
	// TEXT'PTR, ACCORDING TO YEAR'FORM.  UPDATE THE TEXT'PTR.
	// RETURN FALSE IF ANY ERROR IS DETECTED, OTHERWISE RETURN TRUE.

	if (year < MIN_4D_YEAR || year > MAX_4D_YEAR) {
		err_num = DATE_ERR_212;	// BAD YEAR VALUE
		return false;
	};

	char tempyear[5];
	sprintf( tempyear, "%4.4d", year );
	if (date_form.date_type[ index ] != 1) {
		// FORM YY FOR YEAR
		memcpy( text_ptr, &tempyear[2], 3 );
		text_ptr += 2;
	} else {
		// FORM YYYY FOR YEAR
		memcpy( text_ptr, tempyear, 5 );
		text_ptr += 4;
	};

	return true;
};



bool put_sep( bool first_sep, char*& text_ptr, const date_form_type& date_form, long& err_num ) {   
	int sep_form;
	
	if (first_sep)
		sep_form = date_form.sep_one;
	else
		sep_form = date_form.sep_two;

	// MOVE A SEPARATOR INTO BYTE LOCATION POINTED TO BY TEXT'PTR, ACCORDING
	// TO SEP'FORM.  RETURN FALSE IF ANY ERROR IS DETECTED, OTHERWISE RETURN
	// TRUE.
	if (sep_form < 0 || sep_form > 3) {
		err_num = DATE_ERR_215;	// BAD FORM FOR SEP
		return false;
	};

	switch( sep_form ) {
		case 0:	// Blank
//			*text_ptr = ' ';
//			text_ptr++;
			break;

		case 1:	// Slash
			*text_ptr = DATE_SEP;
			text_ptr++;
			break;

		case 2:	// Dash
			*text_ptr = ALT_DATE_SEP;
			text_ptr++;
			break;

		case 3:	// Comma
			memcpy( text_ptr, ", ", 2 );
			text_ptr += 2;
			break;

		case 4:	// Forced dash!
			*text_ptr = '-';
			text_ptr++;
			break;
	};

	return true;
};
	

//
// Converts a gregorian date value to text
//
bool gregorian_to_text( date_conv_type& date, const date_form_type& date_format, char* text_ptr, long& len, long& err_num ) {
	char* text_start = text_ptr;

	// Output date formats
	for (int i=0; i<3; i++) {
		if (i==1)
			put_sep( true, text_ptr, date_format, err_num );
		else if (i==2)
			put_sep( false, text_ptr, date_format, err_num );

		switch( date_format.date_type[i] ) {
			case 0:
			case 1:
			case 2:	// Years
				put_year( date.year, text_ptr, date_format, i, err_num );
				break;

			case 3:
			case 4:
			case 5: // Month
				put_month( date.month, text_ptr, date_format, i, err_num );
				break;

			case 6:
				put_day( date.day, text_ptr, date_format, i, err_num );
				break;
		};
	};

	// Output Time formats
	if (date_format.time_format > 0) {
		int hours;
		char letter = ' ';

		// Special case for time-stamp
		if (date_format.date_type[0] != -1)
			*text_ptr++ = ' ';

		hours = date.hours;
		if (!date_format.time_24hr) {
			if (hours > 11) {
				hours -= 12;
				letter = 'P';
			} else
				letter = 'A';
		};

		if ((date_format.time_format & 1) > 0)
			text_ptr += sprintf( text_ptr, "%2.2d", date.hours );
		
		if ((date_format.time_format & 2) > 0) {
			if (date_format.time_sep)
				*text_ptr++ = ':';
			text_ptr += sprintf( text_ptr, "%2.2d", date.minutes );
		} else if ((date_format.time_format & 4) > 0) {
			if (date_format.time_sep)
				*text_ptr++ = ':';
			text_ptr += sprintf( text_ptr, "%2.2d", date.seconds );
		};

		if (!date_format.time_24hr) {
			*text_ptr++ = letter;
			*text_ptr = 0;
		};
	};

	len = (long)(text_ptr - text_start);

	return true;
};


bool julian_to_text( double JD, const date_form_type& date_format, char* text_ptr, long& len, long& err_num )
{
	// CONVERT A JULIAN DAY NUMBER TO A GREGORIAN DATE AND THEN TO A TEXT STRING,
	// ACCORDING TO DATE_FORMAT.  RETURN FALSE IF ANY ERROR OCCURS.  OTHERWISE,
	// RETURN TRUE WITH JD CONVERTED TO TEXT AND MOVED TO THE BYTE LOCATION
	// STARTING AT TEXT'PTR.
	date_conv_type date;

	if (!convert_to_gregorian( JD, date, err_num )) {
		// REDUNDANT ERROR MESSAGE
		err_num = DATE_ERR_220;	// INVALID JULIAN INPUT
		return false;
	};

	return gregorian_to_text( date, date_format, text_ptr, len, err_num );
};



//**********************************************************
//*                                                        *
//*                      MIS'BAD'JDAY                      *
//*                                                        *
//**********************************************************
bool MIS_BAD_JDAY( double JD, long& err_num ) {
   // RETURNS TRUE IF JD IS INVALID AS A JULIAN DATE,
   // I.E. IS OUTSIDE RANGE 1/1/1950 TO 12/31/2049.
   // RETURNS FALSE IF JD IS VALID, I.E. WITHIN RANGE.

	// 12/31/1949 to 01/01/2050
	if (BAD_JD_LO < JD && JD < BAD_JD_HI) {
		// JULIAN DAY IS WITHIN RANGE
		err_num = 0;
		return false;
	} else {
		// INTERNAL ERROR, DATE OUTSIDE RANGE
		err_num = DATE_ERR_25;
		return true;
	};
};




//**********************************************************
//*                                                        *
//*                  MCONVERT'TO'GREGORIAN                 *
//*                                                        *
//**********************************************************
bool convert_to_gregorian( double JD, date_conv_type& date, long& err_num )
{

	// IF JD IS VALID, PUTS THE GREGORIAN DATE IN D, M, Y. OTHERWISE RETURN FALSE.

/*
	THE MACHINE INSTRUCTIONS IN THE CODE PERFORM THE FOLLOWING           00495000
   SET OF ARITHMETIC CALCULATIONS FROM CACM ALGORITHM 199.              00496000
   THE CALCULATIONS ON THE RIGHT SYMBOLIZE THE REMAINDER FROM           00497000
   THE CORRESPONDING CALCULATION ON THE LEFT.                           00498000
                                                                        00499000
     J := J - 1721119D;                                                 00500000
     Y := (4 * J - 1) / 146097;   J := 4 * J - 1 - 146097 * Y;          00501000
     D := J / 4;                                                        00502000
     J := (4 * D + 3) / 1461;     D := 4 * D + 3 - 1461 * J;            00503000
     D := (D + 4) / 4;                                                  00504000
     M := (5 * D - 3) / 153;      D := 5 * D - 3 - 153 * M;             00505000
     D := (D + 5) / 5;                                                  00506000
     Y := 100 * Y + J;                                                  00507000
                                                                        00508000
     IF M < 10                                                          00509000
      THEN                                                              00510000
         M := M + 3;                                                    00511000
      ELSE                                                              00512000
         M := M - 9;                                                    00513000
         Y := Y + 1;                                                    00514000
*/

	// CHECK FOR VALID JULIAN DATE INPUT
	if (MIS_BAD_JDAY( JD, err_num )) {
		err_num = DATE_ERR_40;
		return false;
	} else {
		double jint, jfrac;
		jfrac = modf( JD+0.5, &jint );

		// Calculate day/month/year
		unsigned long J = (long)jint - 1721119;

		date.year = (4 * J - 1) / 146097;
		J = 4 * J - 1 - 146097 * date.year;
		date.day = J / 4;
		J = (4 * date.day + 3) / 1461;
		date.day = 4*date.day+3 - 1461*J;
		date.day = (date.day+4)/4;
		date.month = (5*date.day-3)/153;
		date.day = 5* date.day-3-153 * date.month;
		date.day = (date.day+5)/5;
		date.year = 100 * date.year + J;

		if (date.month<10)
			date.month += 3;
		else {
			date.month -= 9;
			date.year++;
		};

		// Calculate time
		div_t result;

		// Calculate hours first
		int totalsecs = (int)round(jfrac * 86400.0);
		result = div( totalsecs, 3600 );
		date.hours = result.quot;

		// Minutes and seconds
		result = div( result.rem, 60 );
		date.minutes = result.quot;
		date.seconds = result.rem;
	};

	return true;
};
/*
		// PERFORM CALCULATION DETAILED IN CACM ALGORITHM 199
		(jd - 1721119) * 4 - 1;
      TOS := JD;            << PUSH DOUBLE WORD JULIAN DATE        >>   00529000
      TOS := 1721119D;      << PUSH DOUBLE WORD 1721119            >>   00530000
      ASSEMBLE( DSUB     ;  << SUBTRACT 1721119 FROM JULIAN DATE   >>   00531000
                DASL 2  );  << MULTIPLY DIFFERENCE BY 4            >>   00532000
      TOS := 1D;            << PUSH DOUBLE WORD 1                  >>   00533000
      ASSEMBLE( DSUB    );  << SUBTRACT 1 FROM 'INTERMEDIATE'      >>   00534000
      TOS := 146097D;       << PUSH DOUBLE WORD 146097             >>   00535000
      ASSEMBLE( DDIV    );  << DIVIDE 146097 INTO 'INTER' LEAVING  >>   00536000
                            << 'Y' AS THE 2ND DOUBLE WORD ON STACK >>   00537000
      TOS := -4;            << PUSH BINARY 1111111111111100        >>   00538000
      ASSEMBLE( AND     );  << SIMULATE DIVIDE BY 4 AND MULT BY 4  >>   00539000
      TOS := 3D;            << PUSH DOUBLE WORD 3                  >>   00540000
      ASSEMBLE( DADD    );  << ADD 3 TO 'INTERMEDIATE'             >>   00541000
      TOS := 1461D;         << PUSH DOUBLE WORD 1461               >>   00542000
      ASSEMBLE( DDIV     ;  << DIVIDE 1461 INTO 'INTER' LEAVING    >>   00543000
                            << 'J' AS THE 2ND DOUBLE WORD ON STACK >>   00544000
                DELB     ;  << MAKE REMAINDER 'D' A SINGLE WORD    >>   00545000
                ADDI 4   ;  << ADD 4 TO 'D'                        >>   00546000
                ASR  2   ;  << DIVIDE 'INTER' BY 4                 >>   00547000
                MPYI 5   ;  << MULTIPLY 'D' BY 5                   >>   00548000
                SUBI 3  );  << SUBTRACT 3 FROM 'INTER'             >>   00549000
      TOS := 153;           << PUSH SINGLE WORD 153                >>   00550000
      ASSEMBLE( DIV      ;  << DIVIDE 153 INTO 'INTER' LEAVING     >>   00551000
                            << 'M' AS THE 2ND WORD ON THE STACK    >>   00552000
                ADDI 5   ;  << ADD 5 TO 'D'                        >>   00553000
                DIVI 5  );  << DIVIDE 5 INTO 'INTER'               >>   00554000
      DAY := TOS;           << STORE 'INTER' AS DAY                >>   00555000
      MONTH := TOS;         << STORE 'M' AS MONTH                  >>   00556000
      ASSEMBLE( DELB     ,  << MAKE 'J' A SINGLE WORD              >>   00557000
                XCH      ;  << SWAP 'J' AND LOW ORDER WORD OF 'Y'  >>   00558000
                MPYI 100 ;  << MULTIPLY 'Y' BY 100                 >>   00559000
                ADD     );  << ADD 'INTER' TO 'J'                  >>   00560000
      YEAR := TOS;          << STORE 'INTER' AS YEAR               >>   00561000
      ASSEMBLE( DEL     );  << DELETE HIGH ORDER WORD FROM 'Y'     >>   00562000
                                                                        00563000
      << ADJUST MONTH AND YEAR IF NEEDED >>                             00564000
                                                                        00565000
      TOS := MONTH;         << PUSH MONTH                          >>   00566000
      ASSEMBLE( SUBI 9  );  << SUBTRACT 9 FROM MONTH               >>   00567000
    IF >                    << IF MONTH IS NOT LESS THAN 10        >>   00568000
     THEN                                                               00569000
      YEAR := YEAR + 1                                                  00570000
     ELSE                                                               00571000
      ASSEMBLE( ADDI 12 );  << ADD 12 TO MONTH TO COMPENSATE       >>   00572000
                                                                        00573000
      MONTH := TOS;         << STORE MONTH                         >>   00574000
                                                                        00575000
      MCONVERT'TO'GREGORIAN := TRUE;                                    00576000
    END;                                                                00577000
                                                                        00578000
END;  << MCONVERT'TO'GREGORIAN >>                                       00579000
*/












bool IsNumeric( char test_value ) {
	return ((test_value >= '0' && test_value <= '9'));
};


bool IsAlpha( char test_value ) {
	return ((test_value >= 'A' && test_value <= 'Z') ||
			(test_value >= 'a' && test_value <= 'z'));
};


bool IsAlphaNumeric( char test_value ) {
	return (IsNumeric( test_value ) || IsAlpha( test_value ));
};


void Scan_Blanks( char *& text_ptr, long& err_num ) {
	while (*text_ptr != ';' && *text_ptr == ' ')
		text_ptr++;

	if (*text_ptr == ';')
		err_num = DATE_ERR_310;
};

void Separator( char *& text_ptr, long& err_num ) {
	Scan_Blanks( text_ptr, err_num );

	// Check for any separator, don't care which one
	if (*text_ptr == '/' || *text_ptr == '-' || *text_ptr == ',' || *text_ptr == '.') {
		text_ptr++;
		Scan_Blanks( text_ptr, err_num );
	};
};



bool GetDay( char *& text_ptr, int& day, bool implied_seps, long& err_num ) {
	int n_digits;

	if (IsNumeric(*text_ptr)) {
		// Determine the number of digits
		if (implied_seps)
			n_digits = 2;
		else {
			char* temp = text_ptr;
			while (IsNumeric(*temp))
				temp++;

			n_digits = (long)(temp - text_ptr);
		};

		if (n_digits == 1 || n_digits == 2) {
			day = BINARY( text_ptr, n_digits );
            err_num = DATE_ERR_316;		// CONVERT FAILED

			text_ptr += n_digits;
			return true;
		} else
			err_num = DATE_ERR_320;
	} else
		err_num = DATE_ERR_325;

	return false;
};

   



bool GetMonth( char *& text_ptr, int& month, bool implied_seps, long& err_num ) {
	int n_digits;

	if (IsNumeric( *text_ptr )) {
		// Determine the number of digits
		if (implied_seps)
			n_digits = 2;
		else {
			char* temp = text_ptr;
			while (IsNumeric(*temp))
				temp++;

			n_digits = (long)(temp - text_ptr);
		};

		// MORE CODE GOES HERE

		if (n_digits == 1 || n_digits == 2) {
			month = BINARY( text_ptr, n_digits );

			if (0) {
				err_num = DATE_ERR_316;		// Convert failed
				return false;
			};

			text_ptr += n_digits;
			return true;
		} else
			err_num = DATE_ERR_321;		// Should be 1 or 2 digits
	} else {
		if (IsAlpha( *text_ptr )) {
			int mon_text_length;

			// Count the number of alpha characters
			char* temp = text_ptr;
			while (IsAlpha(*temp))
				temp++;
			mon_text_length = (long)(temp - text_ptr);

            if (1 <= mon_text_length && mon_text_length <= 9) {
				if (convert_month( text_ptr, mon_text_length, month, err_num )) {
					text_ptr += mon_text_length;
					return true;
				};
			} else
				err_num = DATE_ERR_322;		// SHOULD BE 3 TO 9 CHARS
		} else
			err_num = DATE_ERR_326;
	};

	return false;
};





bool GetYear( char *& text_ptr, int& year, bool implied_seps, long& err_num ) {
	int n_digits;

	if (IsNumeric(*text_ptr)) {
		// Determine the number of digits
		if (implied_seps)
			n_digits = 2;
		else {
			char* temp = text_ptr;
			while (IsNumeric(*temp))
				temp++;

			n_digits = (long)(temp - text_ptr);
		};

		if (1 <= n_digits && n_digits <= 4) {
			year = BINARY( text_ptr, n_digits );

			err_num = DATE_ERR_317;		// Convert failed

			text_ptr += n_digits;
			return true;
		} else
			err_num = DATE_ERR_323;		// Should be 1 to 4 digits
	} else	// Not Numeric
		if (IsAlpha( *text_ptr )) {
			M_A0_00_TO_2D( text_ptr, year, err_num );
			if (err_num == 0) {
				// Year conversion succeeded
				text_ptr += 2;
				return true;
			};
		} else
			err_num = DATE_ERR_327;		// Failed to find year

	return false;
};





bool text_to_julian( char* text_ptr, const date_form_type& date_format,
	double& JD, long len, long& err_num )
{

	// SCAN TEXT FROM "TEXT'PTR" UP TO ";" CHARACTER FOR A DATE CONSTANT WITH
	// DATE ORDER AND FORM SPECIFIED BY "DATE'FORMAT".  CONVERT DATE TO JULIAN
	// "JD" AND SET POINTER "END'PTR" JUST PAST LAST CHARACTER IN DATE AND 
	// RETURN "TRUE".  IF AN ERROR IS DETECTED, RETURN "FALSE", NULL JD, AND 
	// N.B.:DEFINES MUST AGREE WITH JULIAN'TO'TEXT AND GET'DATE'FORMAT
	int n_digits;
	date_conv_type date;
	bool date_ok = false;
	bool implied_seps = false;
	const char* save_ptr = text_ptr;

	memset( &date, 0, sizeof( date_conv_type ) );

	// SCAN DATE TEXT AND CONVERT TO DAY, MONTH, YEAR VALUES.
	err_num = 0;

	// Do we have a date type?
	if (date_format.date_type[0] != -1) {
		Scan_Blanks( text_ptr, err_num );

		// NOW DO SPECIAL CHECK FOR IMPLIED SEPARATORS, IE A 6-DIGIT
		// VALUE ONLY, IN WHATEVER DATE ORDER
		if (IsNumeric( *text_ptr )) {
			// Find the special character (the date seperator or the ';' terminating
			// the string.
			const char* temp = text_ptr;
			while (IsAlphaNumeric( *temp ))
				temp++;

			n_digits = (long)(temp - text_ptr);
			if (n_digits > 4)	// MIGHT BE IMP, MIGHT BE BAD
				if (n_digits != 6)		// IS BAD, TO MANY DIGS IN ROW
					err_num = DATE_ERR_329;
				else
					if (text_ptr[n_digits] != ';')	// MORE, SO BAD
						err_num = DATE_ERR_330;
					else	// Is ok
						implied_seps = true;
		};

		// SEE IF OK TILL NOW
		if (err_num) {
			JD = -1;
			err_num = DATE_ERR_312;	// BAD DATE
			return false;
		};

		date_ok = true;
		for (int i=0; i<3 && date_ok; i++) {
			if (i != 0)
				Separator( text_ptr, err_num );

			switch( date_format.date_type[i] ) {
				case 0:
				case 1:
				case 2: // Years
					date_ok &= GetYear( text_ptr, date.year, implied_seps, err_num );
					break;

				case 3:
				case 4:
				case 5: // Months
					date_ok &= GetMonth( text_ptr, date.month, implied_seps, err_num );
					break;

				case 6: // Days
					date_ok &= GetDay( text_ptr, date.day, implied_seps, err_num );
					break;
			};
		};

		// CHECK FOR GOOD DATE, THEN CONVERT TO JULIAN, CHECK AGAIN.
		if (!date_ok) {
			JD = -1;
			err_num = DATE_ERR_312;		// UNABLE TO RECOGNIZE DATE TEXT
			return false;
		};
	};


	// Time cases
	if (date_format.time_format > 0) {
		int sep_len = date_format.time_sep ? 1 : 0;

		// Skip separator for time-stamp
		if (date_format.date_type[0] != -1)
			text_ptr += 1;

		// Get Time
		char temp[3];
		temp[2] = 0;

		// Get the hours
		if ((date_format.time_format & 1) > 0) {
			memcpy( temp, text_ptr, 2 );
			date.hours = atoi( temp );
			text_ptr += 2 + sep_len;
		};

		// Get the minutes
		if ((date_format.time_format & 2) > 0) {
			memcpy( temp, text_ptr, 2 );
			date.minutes = atoi( temp );
			text_ptr += 2 + sep_len;
		};

		// Get the seconds
		if ((date_format.time_format & 4) > 0) {
			memcpy( temp, text_ptr, 2 );
			date.seconds = atoi( temp );
			text_ptr += 2;
		};

		if (!date_format.time_24hr) {
			if (*text_ptr == 'P') {
				// Date object always deals in 24 hour format
				date.hours += 12;
			} else if ((*text_ptr == 'A' || (*text_ptr == 'M')) && date.hours == 12) {
				// Midnight = 0 zeros
				date.hours = 0;
			};
			text_ptr++;
		};
	};

	// DATE TEXT IS GOOD
	len = (long)(text_ptr - save_ptr) + 1;

	// DATE VALIDITY CHECK DONE IN MCONVERT'TO'JULIAN
	if (!convert_to_julian( JD, date, err_num )) {
		err_num = DATE_ERR_313;	// INTERNAL ERROR
		return false;
	} else {
		void* result = memchr( text_ptr, ';', len - (text_ptr - save_ptr) );
		if (!result) {
			err_num = DATE_ERR_311;
			return false;
		};
	};

	return true;
};	// TEXT_TO_JULIAN






//**********************************************************
//*                                                        *
//*                    MLAST'DAY'OF'MONTH                  *
//*                                                        *
//**********************************************************
int MLAST_DAY_OF_MONTH( int month, int year, long& err_num ) {

	// RETURNS THE VALUE OF THE LAST DAY OF GIVEN MONTH IN GIVEN YEAR.

	int DAYS_IN_MONTH[ 12 ] = { 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

	err_num = 0;	// CLEAR ERROR RETURN

	if (month >= 1 && month <= 12 && year >= MIN_4D_YEAR && year <= MAX_4D_YEAR) {
		if (month == 2) {
			// Check for leap year
			if ((year % 4) == 0 && ((year % 400) == 0 || (year % 100) != 0))
				return 29;
			else 
				return 28;
		} else
			return DAYS_IN_MONTH[ month - 1 ];
	} else {
		err_num = DATE_ERR_10;		// Bad Month or Year value
		return -1;
	};
};





//**********************************************************
//*                                                        *
//*                       MIS'BAD'DATE                     *
//*                                                        *
//**********************************************************
bool MIS_BAD_DATE( int day, int month, int year, long& err_num )
{

	// RETURNS TRUE IF MONTH/DAY/YEAR IS AN INVALID GREGORIAN DATE, OR 
	// IF MONTH/DAY/YEAR IS OUTSIDE THE RANGE 1/1/1950 TO 12/31/2049.
	// RETURNS FALSE IF INPUT IS VALID AND WITHIN RANGE.

	if (month >= 1 && month <= 12) {
		if (year >= MIN_4D_YEAR && year <= MAX_4D_YEAR) {		//  1950 TO 2049
			// Do nothing
		} else if (year >= MIDDLE_2D_YEAR && year <= MAX_2D_YEAR) {	// 50 to 99
			// NOTE YEAR IS A VALUE PARAMETER, ONLY CHANGING LOCAL
			year += NINETEEN_HUNDRED;
		} else if (year >= MIN_2D_YEAR && year <= MIDDLE_2D_YEAR) {	// 0 to 49
			year += TWO_THOUSAND;
		} else {
			err_num = DATE_ERR_21;		// Bad Year value
			return true;
		};
	} else {
		err_num = DATE_ERR_20;		// Bad Month value
		return true;
	};


	// Now that year is resolved, and checked, verify the day
	if (day >= 1 && day <= MLAST_DAY_OF_MONTH( month, year, err_num ))
		return false;
	else {
		err_num = DATE_ERR_22;		// Invalid day value;
		return true;
	};
};



//**********************************************************
//*                                                        *
//*                    MCONVERT'TO'JULIAN                  *
//*                                                        *
//**********************************************************
bool convert_to_julian( double& julian_date, date_conv_type& date, long& err_num )
{
	double date_part = 0;
	bool has_date;

/*
   CHECKS DAY, MONTH, YEAR FOR VALID GREGORIAN DATE,                    00389000
   RETURNING FALSE IF GIVEN DATE IS INVALID.                            00390000
   IF GIVEN DATE IS VALID, PUTS THE JULIAN DAY NUMBER                   00391000
   IN JULIAN'DATE AND RETURNS TRUE.                                     00392000
                                                                        00393000
   THE MACHINE INSTRUCTIONS IN THE CODE PERFORM THE FOLLOWING           00394000
   SET OF ARITHMETIC CALCULATIONS FROM CACM ALGORITHM 199.              00395000
   THE CALCULATION FOR 'C' AND 'YA' ACTUALLY REPRESENT A SINGLE         00396000
   DIVISION WITH 'C' AS THE QUOTIENT AND 'YA' AS THE REMAINDER.         00397000
                                                                        00398000
     IF M > 2                                                           00399000
      THEN                                                              00400000
         M := M - 3;                                                    00401000
      ELSE                                                              00402000
         M := M + 9;                                                    00403000
         Y := Y - 1;                                                    00404000
                                                                        00405000
     C := Y / 100;       YA := Y - 100 * C;                             00406000
     J := (146097 * C) / 4 + (1461 * YA) / 4 + (153 * M + 2) / 5 + D    00407000
          + 1721119;                                                    00408000
*/

	// Set correct century if not specified
	// Note YEAR is a Value Parameter, only changing Local value

	if (date.year != 0 && date.month != 0 && date.day != 0) {
		if (MIDDLE_2D_YEAR <= date.year && date.year <= MAX_2D_YEAR)
			date.year += NINETEEN_HUNDRED;
		else if (MIN_2D_YEAR <= date.year && date.year <= MIDDLE_2D_YEAR)
			date.year += TWO_THOUSAND;

		// CHECK FOR VALID DATE INPUT

		// Note: MIS'BAD'DATE calls MLAST'DAY'OF'MONTH that zeroes ERR'NUM
		if (MIS_BAD_DATE( date.day, date.month, date.year, err_num )) {
			err_num = 30;
			return false;
		};

		// ADJUST MONTH AND YEAR IF NEEDED
		if (date.month > 2)
			date.month -= 3;
		else {
			date.month += 9;
			date.year--;
		};

		int c = date.year / 100;
		int ya = date.year - 100 * c;
		date_part = (146097 * c) / 4 + (1461 * ya) / 4 + (153 * date.month + 2 ) / 5 + date.day + 1721119;
		has_date = true;
	} else
		has_date = false;

	double time_port = (((date.hours * 3600) + (date.minutes*60) + date.seconds) / 86400.0);
	if (has_date)
		time_port = time_port - 0.5;

	julian_date = date_part + time_port;

	return true;
};





bool convert_month( char* month_string, int month_string_length, int& month, long& err_num )
{
	// ASSUMES THAT MONTH'STRING CONTAINS ALPHABETIC MONTH.  LENGTH IS 
	// MONTH'STRING'LENGTH OR LESS (I.E. <= 9). IF MONTH'STRING IS INVALID,
	// RETURNS FALSE.  OTHERWISE, PUTS A VALUE FROM 1 TO 12 INTO MONTH
	// AND RETURNS TRUE.  USED BY DAPL RECOGNIZER, INTERPRETER, AND IOS DATE CONVERTER.

	int word_count;
	char *ptr, *f_ptr;

	char MONTH_NAMES[ 108 ] = 
		// Three letter month names
		"APRAUGDECFEBJANJULJUNMARMAYNOVOCTSEP"
		// Four letter month names
		"JULYJUNE"
		// Five letter month names
		"APRILMARCH"
		// Six letter month names
		"AUGUST"
		// Seven letter month names
		"JANUARYOCTOBER"
		// Eight letter month names
		"FEBRUARYDECEMBERNOVEMBER"
		// Nine letter month names
		"SEPTEMBER";

	int MONTH_INDEX[ 11 ] = { 0, 0, 0, 0, 36, 44, 54, 60, 74, 98, 107 };
		// THE SUBSCRIPT IS THE NAME LENGTH, THE VALUE IS THE LOCATION IN 
		// MONTH'NAMES WHERE THE NAMES OF THAT LENGTH BEGIN.

	int MONTH_VALUES[ 23 ] = { 
		4, 8, 12, 2, 1, 7, 6, 3, 5, 11, 10, 9,
		7, 6, 4, 3, 8, 1, 10, 2, 12, 11, 9 };

	int VALUE_INDEX[ 12 ] = { 0, 0, 0, 0, 12, 14, 16, 17, 19, 22 };
		// THE SUBSCRIPT IS THE NAME LENGTH, THE VALUE IS THE LOCATION IN
		// MONTH'VALUES WHERE THE MONTH'VALUES FOR NAMES OF THAT LENGTH START.

	if (month_string_length > 2 && month_string_length < 10) {
		// NAME IN PROPER RANGE
		word_count = 0;

		// Beginning of names
		ptr = &MONTH_NAMES[ MONTH_INDEX[ month_string_length ] ];

		// End of names
		f_ptr = &MONTH_NAMES[ MONTH_INDEX[ month_string_length + 1 ] ];

		while (ptr != f_ptr) {
			if (memcmp( ptr, month_string, month_string_length ) == 0) {
				// Found a match
				month = MONTH_VALUES[ VALUE_INDEX[ month_string_length ] + word_count ];
				return true;
			};

			// NO MATCH
			ptr += month_string_length;

			word_count++;
		};
		// DID NOT FIND MATCH
	};	// Proper range

	month = 0;	// Default value
	err_num = DATE_ERR_200;		// Invalid month name
	return false;
};	// MCONVERT'MONTH







//----------------------------------------------------------
// PROCEDURE M'A0'00'TO'2D:                                 
//                                                          
// This procedure converts a 2-byte year string in A0 or 00 
// format to a 2-digit integer year.                        
//                                                          
// The input value in the string SOURCE is not modified.    
// The output integer YEAR'2D remains unaltered if the      
// conversion fails.                                        
//                                                          
// Return values for ERROR parameter:                       
//                                                          
//    Successful conversion:  0                             
//    Input date contains garbage characters:   DATE'ERR'21 
//    Input date not in acceptable date range:  DATE'ERR'25 
//                                                          
//----------------------------------------------------------
bool M_A0_00_TO_2D( char* source, int& year_2d, long& error )
{
	int i;

	// The following array is a lookup table used to convert
	// the first byte of the A0 or 00 year passed in (in
	// SOURCE(0)) to a 2-digit integer containing value of
	// the corresponding decade.

	long LOOKUP_ONE[ 256 ];
	for (i=0; i<256; i++)
		LOOKUP_ONE[i] = -4096;
	for (i='0'; i<='9'; i++)
		LOOKUP_ONE[i] = 10 * (i-48);
	for (i='A'; i<='Z'; i++) {
		int value = 10 * (i - 'A');
		LOOKUP_ONE[i] = value;
		LOOKUP_ONE[i + ('a' - 'A')] = value;
	};

	// The following array is a lookup table used to convert
	// the second byte of the A0 or 00 year passed in (in
	// SOURCE(1)) to a 2-digit integer which will be added
	// to the decade from the lookup table above.

	long LOOKUP_TWO[ 256 ];
	for (i=0; i<256; i++)
		LOOKUP_TWO[ i ] = -4096;
	for (char c='0'; c<='9'; c++)
		LOOKUP_TWO[ c ] = c - '0';

	// The following array shows all characters allowed in
	// the first byte of the A0 or 00 year passed in, i.e.,
	// in the byte SOURCE(0).  This is used to differentiate
	// between garbage characters and years that are valid
	// numbers, but just outside the allowable year range.
	// This byte contains the character representing the
	// decade.  No such array is needed for the second byte.
	// Years 1950-2049 ("50" to "E9") are considered valid
	// years within the allowable range;  Years 2050-2099
	// ("F0" to "J9") are considered valid, but outside the
	// allowable range.  Years "00" to "49" are interpreted
	// to mean years 2000-2049.

	char VALID_DECADES[ 256 ];
	memset( VALID_DECADES, '?', 256 );
	for (char c = 0; c<=9; c++) {
		VALID_DECADES[ c + '0' ] = c + '0';
		VALID_DECADES[ c + 'A' ] = c + 'A';
		VALID_DECADES[ c + 'a' ] = c + 'a';
	};

	// The following DOUBLE is used for local a work area
	// to store interim conversions prior to knowing whether
	// the conversion will actually succeed.  If the input
	// year contains a garbage character or a year that is
	// out of range, we don't want to modify the output
	// parameter until we know the conversion is guaranteed
	// to succeed.

	long local_2d_year;

	// Do the conversion one byte at a time, starting with
	// the least significant byte, SOURCE(1).  Working
	// backwards like this simplifies the subsequent logic
	// for examining the most significant byte.  Convert the
	// ASCII value of the byte in SOURCE(1) to its numeric
	// equivalent, and store the result in double integer
	// LOCAL'2D'YEAR.

	local_2d_year = LOOKUP_TWO[ source[1] ];

	// Examine the value of the LOCAL'2D'YEAR work variable
	// to see if its value is negative.  A negative value
	// indicates that the byte contains a garbage character.
	if (local_2d_year < 0) {
		// A garbage character was encountered in the
		// second byte of the input year.
		error = DATE_ERR_21;
		return false;
	};

	// Now convert the ASCII value of the most significant
	// byte, in SOURCE(0), to its corresponding numeric
	// equivalent, and add the result to the existing value
	// of double integer LOCAL'2D'YEAR.

	local_2d_year += LOOKUP_ONE[ source[0] ];

	// Examine the value of the LOCAL'2D'YEAR work variable
	// to see if its value is negative.  A negative value
	// indicates that the byte in SOURCE(0) contains either
	// a gargage character, or a year that is out of range.
	// If the result is negative, we have to probe further
	// to determine which of these two is the cause of the
	// failed conversion.

	if (local_2d_year < 0) {
		if (VALID_DECADES[ source[0] ] == '?') {
			// A garbage character was encountered in the
			// first byte of the input year.
			error = DATE_ERR_21;
			return false;
		} else {
			// The year passed in a valid number, but it is
			// outside the range of years allowed.
			error = DATE_ERR_25;
			return false;
		};
	};

	// If you reach here, the conversion was successful.
	// Now move the contents of the working buffer
	// LOCAL'2D'YEAR to the output parameter YEAR'2D.

	year_2d = local_2d_year;
	error = 0;

	return true;
};	// M'A0'00'TO'2D







//----------------------------------------------------------
// PROCEDURE M'A0'00'TO'A0:                                 
//                                                          
// This procedure converts a 2-byte year string in A0 or 00 
// format to a 2-byte year string in A0 format.             
//                                                          
// The input value in the string SOURCE is not modified.    
// The output string TARGET remains unaltered if the        
// conversion fails.                                        
//                                                          
// Return values for ERROR parameter:                       
//                                                          
//    Successful conversion:  0                             
//    Input date contains garbage characters:   DATE'ERR'21 
//    Input date not in acceptable date range:  DATE'ERR'25 
//                                                          
//----------------------------------------------------------
bool M_A0_00_TO_A0( char* source, char* target, long& error ) 
{
	// The following array is used to convert the first byte of the A0 or 00
	// year passed in (in SOURCE(0)) to the appropriate A0-format output value
	// in TARGET(0).
	char LOOKUP_ONE[ 256 ];
	memset( LOOKUP_ONE, '?', 256 );
	memcpy( &LOOKUP_ONE[48], "ABCDE56789", 10 );
	memcpy( &LOOKUP_ONE[65], "ABCDE", 5 );
	memcpy( &LOOKUP_ONE[97], "ABCDE", 5 );

	// The following array is used to convert the second byte of the A0 or 00 year
	// passed in (in SOURCE(1)) to the appropriate A0-format output value in TARGET(1).
	char LOOKUP_TWO[ 256 ];
	memset( LOOKUP_TWO, '?', 256 );
	memcpy( &LOOKUP_TWO[48], "0123456789", 10 );

	// The following array shows all characters allowed in the first byte of the A0
	// year passed in, i.e., in the byte SOURCE(0).  This is used to differentiate 
	// between garbage characters and years that are valid numbers, but just outside
	// the allowable year range.

	// This byte contains the character representing the decade.  No such array is
	// needed for the second byte.  Years 1950-2049 ("50" to "E9") are considered
	// valid years within the allowable range;  Years 2050-2099 ("F0" to "J9") are
	// considered valid, but outside the allowable range.  Years "00" to "49" are
	// interpreted to mean years 2000-2049.
	char VALID_DECADES[ 256 ];
	memset( VALID_DECADES, '?', 256 );
	memcpy( &VALID_DECADES[48], "0123456789", 10 );
	memcpy( &VALID_DECADES[65], "ABCDEFGHIJ", 10 );
	memcpy( &VALID_DECADES[97], "abcdefghij", 10 );

	// The following buffer is used for local a work area to store interim 
	// conversions prior to knowing whether the conversion will actually succeed.
	// This is needed because in some cases, the caller will use the same parameter
	// for both input and output.  If the input year contains a garbage character
	// or a year that is out of range, we don't want to clobber it with the output
	// until we know the conversion is guaranteed to succeed.
	char local_string_year[2];

	local_string_year[0] = LOOKUP_ONE[ source[0] ];
	local_string_year[1] = LOOKUP_TWO[ source[1] ];

	// Examine the two bytes in the LOCAL_STRING_YEAR work buffer to determine whether
	// the conversion failed.  If it did, a "?" will appear in either or both bytes. 
	// When this happens, we have to probe further to determine the reason for the
	// failed conversion: either the input year contained a garbage character, or it
	// was out of the range of years allowed.

	// By examining the second byte first we can simplify the subsequent logic for
	// examining the first byte.
	if (local_string_year[1] == '?') {
		// A garbage character was encountered in the second byte of the input year.
		error = DATE_ERR_21;
		return false;
	};

	if (local_string_year[0] == '?') {
		if (VALID_DECADES[ source[0] ] == '?') {
			// A garbage character was encountered in the first byte of the input year.
			error = DATE_ERR_21;
			return false;
		} else {
			// The year passed in a valid number, but it is outside the range of 
			// years allowed.
			error = DATE_ERR_25;
			return false;
		};
	};

	// If you reach here, the conversion was successful.  Now move the contents of
	// the working buffer LOCAL'STRING'YEAR to the output parameter TARGET.
	memcpy( target, local_string_year, 2 );
	error = 0;
	return true;
};	// M_A0_00_TO_A0




